
import logging
import logging.config
import os
import sys
import types
import pathlib
import yaml

__all__ = ['debug', 'info', 'warn', 'error', 'critical', 'log']
__this_dir__ = pathlib.Path(os.path.dirname(os.path.abspath(__file__)))
for config in ['log.yaml', __this_dir__/ 'log.yaml']:
    if os.path.exists(config):
        with open(config) as f:
            config = yaml.safe_load(f)
        logging.config.dictConfig(config)
        break
else:
    raise FileNotFoundError('No log config file found')


def _is_internal_frame(frame):
    _srcfile = os.path.normcase(_is_internal_frame.__code__.co_filename)
    """Signal whether the frame is a CPython or logging module internal."""
    filename = os.path.normcase(frame.f_code.co_filename)
    return filename == _srcfile or (
        "importlib" in filename and "_bootstrap" in filename
    )

if hasattr(sys, "_getframe"):
    currentframe = lambda: sys._getframe(1)
else: #pragma: no cover
    def currentframe():
        """Return the frame object for the caller's stack frame."""
        try:
            raise Exception
        except Exception as exc:
            return exc.__traceback__.tb_frame.f_back

def _find_caller(stacklevel=1):
    """
    Find the stack frame of the caller so that we can note the source
    file name, line number and function name.
    """
    f = currentframe()
    #On some versions of IronPython, currentframe() returns None if
    #IronPython isn't run with -X:Frames.
    if f is None:
        return None
    while stacklevel > 0:
        next_f = f.f_back
        if next_f is None:
            ## We've got options here.
            ## If we want to use the last (deepest) frame:
            break
            ## If we want to mimic the warnings module:
            #return ("sys", 1, "(unknown function)", None)
            ## If we want to be pedantic:
            #raise ValueError("call stack is not deep enough")
        f = next_f
        if not _is_internal_frame(f):
            stacklevel -= 1
    return f

def _find_caller_module(stacklevel=1):
    f = _find_caller(stacklevel)
    return sys.modules.get(f.f_globals.get("__name__"))

def _find_module_loggers(module):
    return {attr : getattr(module, attr) for attr in dir(module) \
            if isinstance(getattr(module, attr), logging.Logger)}


KNOWN_MODULE_LOGGERS = {}
def logger_for_module(func):
    def wrapper(*args, **kwargs):
        # find logger of caller module
        m = _find_caller_module()
        assert isinstance(m, types.ModuleType)
        if m not in KNOWN_MODULE_LOGGERS:
            loggers = _find_module_loggers(m)
            if not bool(loggers):
                logger = logging.getLogger(m.__name__)
            else:
                logger = loggers.values()[0]
            KNOWN_MODULE_LOGGERS[m] = logger
        else:
            logger = KNOWN_MODULE_LOGGERS[m]

        # find function in caller globals
        funcs = [(n,getattr(m, n)) for n in dir(m) if getattr(m, n) is wrapper]
        _logger_func = getattr(logger, func.__name__)
        if len(funcs):
            # When directly call the function, replace the function with logger.
            for n, _ in funcs:
                setattr(m, n, _logger_func)
        return _logger_func(*args, **kwargs)
    return wrapper

@logger_for_module
def debug(msg, *args, **kwargs):
    pass
@logger_for_module
def info(msg, *args, **kwargs):
    pass
@logger_for_module
def warning(msg, *args, **kwargs):
    pass
@logger_for_module
def warn(msg, *args, **kwargs):
    pass
@logger_for_module
def error(msg, *args, **kwargs):
    pass
@logger_for_module
def exception(msg, *args, exc_info=True, **kwargs):
    pass
@logger_for_module
def critical(msg, *args, **kwargs):
    pass
@logger_for_module
def fatal(msg, *args, **kwargs):
    pass

@logger_for_module
def log(level, msg, *args, **kwargs): # pragma: no cover
    pass